﻿using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Threading;
using System.Threading.Tasks;
using Volo.Abp;
using Volo.Abp.Auditing;
using Volo.Abp.Data;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Domain.Entities;
using Volo.Abp.Domain.Repositories;
using Volo.Abp.Domain.Repositories.EntityFrameworkCore;
using Volo.Abp.EntityFrameworkCore;
using Volo.Abp.Guids;
using Volo.Abp.MultiTenancy;
using Volo.Abp.Users;

namespace Xms.Abp.Storage;

[Dependency(ReplaceServices = true)]
[ExposeServices(typeof(IXmsStorageBulkOperationProvider), typeof(IEfCoreBulkOperationProvider))]
public class XmsStorageBulkOperationProvider : IXmsStorageBulkOperationProvider, ISingletonDependency
{
    public IGuidGenerator GuidGenerator { get; set; }
    public XmsStorageBulkOperationProvider(IGuidGenerator guidGenerator)
    {
        GuidGenerator = guidGenerator;
    }
    protected virtual async Task SaveChangesAsync<TEntity>(IEfCoreRepository<TEntity> repository, bool autoSave, CancellationToken cancellationToken = default)
        where TEntity : class, IEntity
    {
        if (autoSave)
        {
            await (await repository.GetDbContextAsync()).SaveChangesAsync(cancellationToken);
        }
    }
    protected virtual TService LazyGetRequiredService<TEntity, TService>(IRepository<TEntity> repository)
        where TEntity : class, IEntity
    {
        var repo = repository as BasicRepositoryBase<TEntity>;
        return repo.LazyServiceProvider.LazyGetRequiredService<TService>();
    }
    protected virtual TService LazyGetService<TEntity, TService>(IRepository<TEntity> repository)
        where TEntity : class, IEntity
    {
        var repo = repository as BasicRepositoryBase<TEntity>;
        return repo.LazyServiceProvider.LazyGetService<TService>();
    }
    public async Task DeleteManyAsync<TDbContext, TEntity>(IEfCoreRepository<TEntity> repository, IEnumerable<TEntity> entities, bool autoSave, CancellationToken cancellationToken)
        where TDbContext : IEfCoreDbContext
        where TEntity : class, IEntity
    {
        if (typeof(ISoftDelete).IsAssignableFrom(typeof(TEntity))
            && LazyGetRequiredService<TEntity, IDataFilter>(repository).IsEnabled<ISoftDelete>())
        {
            var currentUser = LazyGetRequiredService<TEntity, ICurrentUser>(repository);
            foreach (var entity in entities)
            {
                if (entity is IDeletionAuditedObject deletion)
                {
                    deletion.IsDeleted = true;
                    deletion.DeletionTime = DateTime.Now;
                    deletion.DeleterId = currentUser.Id;
                }
                else if (entity is ISoftDelete ent)
                {
                    ent.IsDeleted = true;
                    if (ent is IHasDeletionTime ent3)
                    {
                        ent3.DeletionTime = DateTime.Now;
                    }
                }
                if (entity is IHasConcurrencyStamp ent2)
                {
                    ent2.ConcurrencyStamp = Guid.NewGuid().ToString("n");
                }
            }
            await (await repository.GetDbSetAsync()).BulkUpdateAsync(entities, cancellationToken);
        }
        else
        {
            await (await repository.GetDbSetAsync()).BulkDeleteAsync(entities, cancellationToken);
        }
        await SaveChangesAsync(repository, autoSave, cancellationToken);
    }

    public async Task DeleteManyAsync<TDbContext, TEntity>(IEfCoreRepository<TEntity> repository, Expression<Func<TEntity, bool>> predicate, bool autoSave, CancellationToken cancellationToken)
        where TDbContext : IEfCoreDbContext
        where TEntity : class, IEntity
    {
        var query = (await repository.GetDbSetAsync()).Where(predicate);
        var dataFilter = LazyGetRequiredService<TEntity, IDataFilter>(repository);
        if (typeof(IMultiTenant).IsAssignableFrom(typeof(TEntity)) && dataFilter.IsEnabled<IMultiTenant>())
        {
            var currentTenant = LazyGetRequiredService<TEntity, ICurrentTenant>(repository);
            query = query.Where(r => EF.Property<Guid?>(r, nameof(IMultiTenant.TenantId)) == currentTenant.Id);
        }
        if (typeof(ISoftDelete).IsAssignableFrom(typeof(TEntity)) && dataFilter.IsEnabled<ISoftDelete>())
        {
            var currentUser = LazyGetRequiredService<TEntity, ICurrentUser>(repository);
            var dict = new Dictionary<string, object>();
            if (typeof(IDeletionAuditedObject).IsAssignableFrom(typeof(TEntity)))
            {
                dict[nameof(IDeletionAuditedObject.IsDeleted)] = true;
                dict[nameof(IDeletionAuditedObject.DeleterId)] = currentUser.Id;
                dict[nameof(IDeletionAuditedObject.DeletionTime)] = DateTime.Now;
            }
            else if (typeof(ISoftDelete).IsAssignableFrom(typeof(TEntity)))
            {
                dict[nameof(ISoftDelete.IsDeleted)] = true;
                if (typeof(IHasDeletionTime).IsAssignableFrom(typeof(TEntity)))
                {
                    dict[nameof(IHasDeletionTime.DeletionTime)] = DateTime.Now;
                }
            }
            if (typeof(IHasConcurrencyStamp).IsAssignableFrom(typeof(TEntity)))
            {
                dict[nameof(IHasConcurrencyStamp.ConcurrencyStamp)] = Guid.NewGuid().ToString("n");
            }
            await query.UpdateFromQueryAsync(dict, cancellationToken);
        }
        else
        {
            await query.DeleteFromQueryAsync(cancellationToken);
        }
        await SaveChangesAsync(repository, autoSave, cancellationToken);
    }

    public async Task InsertManyAsync<TDbContext, TEntity>(IEfCoreRepository<TEntity> repository, IEnumerable<TEntity> entities, bool autoSave, CancellationToken cancellationToken)
        where TDbContext : IEfCoreDbContext
        where TEntity : class, IEntity
    {
        var currentTenant = LazyGetRequiredService<TEntity, ICurrentTenant>(repository);
        var currentUser = LazyGetRequiredService<TEntity, ICurrentUser>(repository);
        var dataFilter = LazyGetRequiredService<TEntity, IDataFilter>(repository);
        var tenantEnable = dataFilter.IsEnabled<IMultiTenant>();
        foreach (var entity in entities)
        {
            if (entity is ICreationAuditedObject ent1)
            {
                ObjectHelper.TrySetProperty(ent1, r => r.CreatorId, () => currentUser.Id);
            }
            if (entity is IHasCreationTime ent2)
            {
                ObjectHelper.TrySetProperty(ent2, r => r.CreationTime, () => DateTime.Now);
            }
            if (entity is IModificationAuditedObject ent3)
            {
                ObjectHelper.TrySetProperty(ent3, r => r.LastModifierId, () => currentUser.Id);
            }
            if (entity is IHasModificationTime ent4)
            {
                ObjectHelper.TrySetProperty(ent4, r => r.LastModificationTime, () => DateTime.Now);
            }
            if (entity is IHasConcurrencyStamp ent5)
            {
                ent5.ConcurrencyStamp = GuidGenerator.Create().ToString("n");
            }
            if (tenantEnable && entity is IMultiTenant ent6)
            {
                ObjectHelper.TrySetProperty(ent6, r => r.TenantId, () => currentTenant.Id);
            }
            if (tenantEnable && entity is IEntity<Guid> ent7 && ent7.Id == default)
            {
                ObjectHelper.TrySetProperty(ent7, r => r.Id, () => GuidGenerator.Create());
            }
        }
        await (await repository.GetDbSetAsync()).BulkInsertAsync(entities, cancellationToken);
        await SaveChangesAsync(repository, autoSave, cancellationToken);
    }

    public async Task UpdateManyAsync<TDbContext, TEntity>(IEfCoreRepository<TEntity> repository, IEnumerable<TEntity> entities, bool autoSave, CancellationToken cancellationToken)
        where TDbContext : IEfCoreDbContext
        where TEntity : class, IEntity
    {
        var currentUser = LazyGetRequiredService<TEntity, ICurrentUser>(repository);
        foreach (var entity in entities)
        {
            if (entity is IModificationAuditedObject ent1)
            {
                ent1.LastModifierId = currentUser.Id;
            }
            if (entity is IHasModificationTime ent2)
            {
                ent2.LastModificationTime = DateTime.Now;
            }
            if (entity is IHasConcurrencyStamp ent3)
            {
                ent3.ConcurrencyStamp = Guid.NewGuid().ToString("n");
            }
        }
        await (await repository.GetDbSetAsync()).BulkUpdateAsync(entities, cancellationToken);
        await SaveChangesAsync(repository, autoSave, cancellationToken);
    }

    public async Task UpdateManyAsync<TDbContext, TEntity>(IEfCoreRepository<TEntity> repository, Expression<Func<TEntity, bool>> predicate, Expression<Func<TEntity, TEntity>> updateFactory, bool autoSave, CancellationToken cancellationToken)
        where TDbContext : IEfCoreDbContext
        where TEntity : class, IEntity
    {
        var query = (await repository.GetDbSetAsync()).Where(predicate);
        var dataFilter = LazyGetRequiredService<TEntity, IDataFilter>(repository);
        if (typeof(IMultiTenant).IsAssignableFrom(typeof(TEntity)) && dataFilter.IsEnabled<IMultiTenant>())
        {
            var currentTenant = LazyGetRequiredService<TEntity, ICurrentTenant>(repository);
            query = query.Where(r => EF.Property<Guid?>(r, nameof(IMultiTenant.TenantId)) == currentTenant.Id);
        }
        await query.UpdateFromQueryAsync(updateFactory, cancellationToken);
        var dict = new Dictionary<string, object>();
        if (typeof(IModificationAuditedObject).IsAssignableFrom(typeof(TEntity)))
        {
            var currentUser = LazyGetRequiredService<TEntity, ICurrentUser>(repository);
            dict[nameof(IModificationAuditedObject.LastModifierId)] = currentUser.Id;
        }
        if (typeof(IHasModificationTime).IsAssignableFrom(typeof(TEntity)))
        {
            dict[nameof(IModificationAuditedObject.LastModificationTime)] = DateTime.Now;
        }
        if (typeof(IHasConcurrencyStamp).IsAssignableFrom(typeof(TEntity)))
        {
            dict[nameof(IHasConcurrencyStamp.ConcurrencyStamp)] = Guid.NewGuid().ToString("n");
        }
        if (dict.Count > 0)
        {
            await query.UpdateFromQueryAsync(dict, cancellationToken);
        }
        await SaveChangesAsync(repository, autoSave, cancellationToken);
    }

    public async Task UpdateManyAsync<TDbContext, TEntity>(IEfCoreRepository<TEntity> repository, Expression<Func<TEntity, bool>> predicate, Dictionary<string, object> values, bool autoSave, CancellationToken cancellationToken)
        where TDbContext : IEfCoreDbContext
        where TEntity : class, IEntity
    {
        var query = (await repository.GetDbSetAsync()).Where(predicate);
        var dataFilter = LazyGetRequiredService<TEntity, IDataFilter>(repository);
        if (typeof(IMultiTenant).IsAssignableFrom(typeof(TEntity)) && dataFilter.IsEnabled<IMultiTenant>())
        {
            var currentTenant = LazyGetRequiredService<TEntity, ICurrentTenant>(repository);
            query = query.Where(r => EF.Property<Guid?>(r, nameof(IMultiTenant.TenantId)) == currentTenant.Id);
        }
        if (typeof(IModificationAuditedObject).IsAssignableFrom(typeof(TEntity)) && !values.ContainsKey(nameof(IModificationAuditedObject.LastModifierId)))
        {
            var currentUser = LazyGetRequiredService<TEntity, ICurrentUser>(repository);
            values[nameof(IModificationAuditedObject.LastModifierId)] = currentUser.Id;
        }
        if (typeof(IHasModificationTime).IsAssignableFrom(typeof(TEntity)) && !values.ContainsKey(nameof(IModificationAuditedObject.LastModificationTime)))
        {
            values[nameof(IModificationAuditedObject.LastModificationTime)] = DateTime.Now;
        }
        if (typeof(IHasConcurrencyStamp).IsAssignableFrom(typeof(TEntity)) && !values.ContainsKey(nameof(IHasConcurrencyStamp.ConcurrencyStamp)))
        {
            values[nameof(IHasConcurrencyStamp.ConcurrencyStamp)] = Guid.NewGuid().ToString("n");
        }
        if (values.Count > 0)
        {
            await query.UpdateFromQueryAsync(values, cancellationToken);
        }
        await SaveChangesAsync(repository, autoSave, cancellationToken);
    }
}
